package map;

//TODO: NEEDS IMPROVED COMMENTS

import java.io.Serializable;
import java.util.Observable;

//TODO: MAKE ITEM METHODS ACCOUNT FOR LARGE ITEMS
/**
 * Models a map as an array of squares. Each square has a number
 * corresponding to an entry in a MapLibrary.
 *
 * @author Alex Babis
 */
public class MapData extends Observable implements Serializable
{
    private static final long serialVersionUID = 1L;
	protected int width;
	protected int height;
	protected MapDataTerrainInstance[][] terrain;
	protected MapDataItemInstance[][] items;
	
	/**
	 * Builds a map
	 * @param library	The terrain and item library to be used in this map
	 * @param width		The width of this map in squares
	 * @param height	The height of this map in squares
	 */
	public MapData(int width, int height)
	{
		this.width = width;
		this.height = height;
		terrain = new MapDataTerrainInstance[width][height]; // Column-major ordering
		items = new MapDataItemInstance[width][height];
	}

	//TODO: TEST
	/**
	 * Changes the size of the map. If the map is shrunk,
	 * it can be later re-grown without loss of information
	 * @param width		The new width of this map in squares
	 * @param height	The new height of this map in squares
	 */
	public void setDimensions(int width, int height)
	{
		MapDataTerrainInstance[][] newTerrainArray = new MapDataTerrainInstance[width][height];
		MapDataItemInstance[][] newItemArray = new MapDataItemInstance[width][height];
		int travWidth = Math.min(width, terrain.length);
		int travHeight = Math.min(height, terrain[0].length);
		for(int x = 0; x < travWidth; x++)
		{
			for(int y = 0; y < travHeight; y++)
			{
				newTerrainArray[x][y] = terrain[x][y];
				newItemArray[x][y] = items[x][y];
			}
		}
		terrain = newTerrainArray;
		items = newItemArray;
		this.width = width;
		this.height = height;
		this.setChanged();
		this.notifyObservers(FullMapUpdate.class);
	}

	/** Gets the width of this map
	 * @return The width of this map */
	public int getWidth()
	{
		return width;
	}

	/** Gets the height of this map
	 * @return The height of this map */
	public int getHeight()
	{
		return height;
	}

	/** Adds a terrain tile at the specified location
	 * @param x 		X coordinate where tile is to be placed
	 * @param y 		Y coordinate where tile is to be placed
	 * @param id 		ID of tile type
	 * @param rotation  Rotation of tile in quarter-turns clockwise */
	public void setTerrain(int x, int y, int id, int rotation)
	{
		terrain[x][y] = new MapDataTerrainInstance(id, rotation);
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}
	
	/** Replaces terrain array with passed array
	 * @param terrain 		The new array */
	public void setTerrain(MapDataTerrainInstance[][] terrain)
	{
		this.terrain = terrain;
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}

	/** Removes terrain tile at the specified location
	 * @param x 		X coordinate where tile is to be removed
	 * @param y 		Y coordinate where tile is to be removed */
	public void removeTerrain(int x, int y)
	{
		terrain[x][y] = null;
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}

	/** Gets the ID of this tile corresponding to a library terrain entry
	 * @return The ID of this tile corresponding to a library terrain entry */
	public int getTerrainType(int x, int y)
	{
		return terrain[x][y].getType();
	}

	/** Rotates terrain tile at the specified location clockwise
	 * @param x 		X coordinate of tile to be rotated
	 * @param y 		Y coordinate of tile to be rotated */
	public void rotateTerrainCW(int x, int y)
	{
		terrain[x][y].rotateCW();
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}

	/** Rotates terrain tile at the specified location counter-clockwise
	 * @param x 		X coordinate of tile to be rotated
	 * @param y 		Y coordinate of tile to be rotated */
	public void rotateTerrainCCW(int x, int y)
	{
		terrain[x][y].rotateCCW();
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}

	/** Sets the rotation of the tile at the specified location
	 * in quarter-turns clockwise
	 * @param x 		X coordinate of tile to be rotated
	 * @param y 		Y coordinate of tile to be rotated
	 * @param rotation 	New rotation in quarter-turns clockwise */
	public void setTerrainRotation(int x, int y, int rotation)
	{
		terrain[x][y].setRotation(rotation);
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}

	/** Gets the rotation of the terrain tile at specified location
	 * @param x 		X coordinate of tile
	 * @param y 		Y coordinate of tile
	 * @return The rotation of this terrain tile in quarter-turns clockwise*/
	public int getTerrainRotation(int x, int y)
	{
		return terrain[x][y].getRotation();
	}

	/** Adds an item at the specified location
	 * @param x 		X coordinate where item is to be placed
	 * @param y 		Y coordinate where item is to be placed
	 * @param id 		ID of item type
	 * @param rotation  Rotation of item in quarter-turns clockwise */
	public void setItem(int x, int y, int id, int rotation, int width, int height)
	{
		items[x][y] = new MapDataItemInstance(id, rotation, width, height);
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
		// TODO: ELSE THROW EXCEPTION
	}

	/** Removes item at the specified location
	 * @param x 		X coordinate where item is to be removed
	 * @param y 		Y coordinate where item is to be removed */
	public void removeItem(int x, int y)
	{
		MapDataItemInstance item = getItem(x, y);
		if(item != null)
		{
			for(int i = 0; i < items.length; i++)
			{
				for(int j = 0; j < items[i].length; j++)
				{
					if(items[i][j] == item)
						items[i][j] = null;
				}
			}
		}
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}
	/** Moves item at the specified location to new location
     * @param x         X coordinate where item is
     * @param y         Y coordinate where item is
     * @param newX      New X coordinate
     * @param new Y     New Y coordinate*/
	public void moveItem(int x, int y, int newX, int newY)
	{
	    MapDataItemInstance item = items[x][y];
	    items[x][y] = null;
	    items[newX][newY] = item;
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}

	/** Gets the ID of this item corresponding to a library item entry
	 * @return The ID of this item corresponding to a library item entry */
	public int getItemType(int x, int y)
	{
		return getItem(x, y).getType();
	}

	/** Rotates item at the specified location clockwise
	 * @param x 		X coordinate of item to be rotated
	 * @param y 		Y coordinate of item to be rotated */
	public void rotateItemCW(int x, int y)
	{
		getItem(x, y).rotateCW();
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}

	/** Rotates item at the specified location counter-clockwise
	 * @param x 		X coordinate of item to be rotated
	 * @param y 		Y coordinate of item to be rotated */
	public void rotateItemCCW(int x, int y)
	{
		getItem(x, y).rotateCCW();
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}

	/** Sets the rotation of the item at the specified location
	 * in quarter-turns clockwise
	 * @param x 		X coordinate of item to be rotated
	 * @param y 		Y coordinate of item to be rotated
	 * @param rotation 	New rotation in quarter-turns clockwise */
	public void setItemRotation(int x, int y, int rotation)
	{
		getItem(x, y).setRotation(rotation);
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}

	/** Gets the rotation of the item at specified location
	 * @param x 		X coordinate of item
	 * @param y 		Y coordinate of item
	 * @return The rotation of item in quarter-turns clockwise*/
	public int getItemRotation(int x, int y)
	{
		return getItem(x, y).getRotation();
	}

	/** Gets the width in squares of the item at specified coordinate
	 * @param x 		X coordinate of item
	 * @param y 		Y coordinate of item
	 * @return The width in squares of the item at specified coordinate*/
	public int getItemWidth(int x, int y)
	{
		MapDataItemInstance item = getItem(x, y);
		if(item == null)
			return -1;
		return item.getWidth();
	}
	
	/** Gets the height in squares of the item at specified coordinate
	 * @param x 		X coordinate of item
	 * @param y 		Y coordinate of item
	 * @return The height in squares of the item at specified coordinate*/
	public int getItemHeight(int x, int y)
	{
		MapDataItemInstance item = getItem(x, y);
		if(item == null)
			return -1;
		return item.getHeight();
	}

	/** Tells if location has a terrain tile
	 * This method is not the logical negation of the method hasNoTerrainAt
	 * because both methods return false if coordinates
	 * are out of bounds
	 * @param x 		X coordinate of query location
	 * @param y 		Y coordinate of query location
	 * @return True if location has an terrain; false otherwise*/
	public boolean hasTerrainAt(int x, int y)
	{
		return x >= 0 && y >= 0 &&
			x < terrain.length &&
			y < terrain[0].length &&
			terrain[x][y] != null;
	}

	/** Tells if location has no terrain tile
	 * This method is not the logical negation of the method hasTerrainAt
	 * because both methods return false if coordinates
	 * are out of bounds
	 * @param x 		X coordinate of query location
	 * @param y 		Y coordinate of query location
	 * @return True if location has an terrain; false otherwise*/
	public boolean hasNoTerrainAt(int x, int y)
	{
		return x >= 0 && y >= 0 &&
			x < terrain.length &&
			y < terrain[0].length &&
			terrain[x][y] == null;
	}

	/** Tells if location has an item covering it
	 * @param x 		X coordinate of query location
	 * @param y 		Y coordinate of query location
	 * @return True if location has an item; false otherwise*/
	public boolean hasItemAt(int x, int y)
	{
		return getItem(x, y) != null;
	}

	/** Tells if location stores an item in the array
	 * @param x 		X coordinate of query location
	 * @param y 		Y coordinate of query location
	 * @return True if location has an item; false otherwise*/
	public boolean hasItemLocatedAt(int x, int y)
	{
		return x >= 0 && y >= 0 &&
			x < items.length && y < items[0].length &&
			items[x][y] != null;
	}
	
	/** Gets the array location of item covering this square
	 * @param x 		X coordinate of query location
	 * @param y 		Y coordinate of query location
	 * @return The array location of item covering this square*/
	public int[] getItemLocation(int x, int y)
	{
		for(int i = items.length - 1; i >= 0; i--)
		{
			for(int j = items[i].length - 1; j >= 0 ; j--)
			{
				MapDataItemInstance item = items[i][j];
				if(item != null)
				{
					if(item.getRotation() % 2 == 0 && i <= x && 
							i + items[i][j].getWidth() > x
							&& j <= y && j + items[i][j].getHeight() > y)
						return new int[]{i, j};
					else if(item.getRotation() % 2 == 1 && i <= x && 
							i + items[i][j].getHeight() > x
							&& j <= y && j + items[i][j].getWidth() > y)
						return new int[]{i, j};
				}
			}
		}
		return null;
	}

	//TODO: MAKE MOAR EFFICIENT
	/** Removes all entries from this map*/
	public void clear()
	{
		items = new MapDataItemInstance[items.length][items[0].length];
		terrain = new MapDataTerrainInstance[terrain.length][terrain[0].length];
        this.setChanged();
        this.notifyObservers(FullMapUpdate.class);
	}
	
	public boolean hasNoTerrain()
	{
		for(int x = 0; x < items.length; x++)
		{
			for(int y = 0; y < items[x].length; y++)
			{
				if(terrain[x][y] != null)
					return false;
			}
		}
		return true;
	}
	
	/**
	 * Because items can be larger than 1x1,
	 * an item may cover a square without
	 * occupying that square in the array representation.
	 * This method finds the item, if any,
	 * that covers the specified location
	 * @param x 		X coordinate of query location
	 * @param y 		Y coordinate of query location
	 * @return The item, if any, at the specified location
	 */
	protected MapDataItemInstance getItem(int x, int y)
	{
		for(int i = items.length - 1; i >= 0; i--)
		{
			for(int j = items[i].length - 1; j >= 0 ; j--)
			{
				MapDataItemInstance item = items[i][j];
				if(item != null)
				{
					if(item.getRotation() % 2 == 0 && i <= x && 
							i + item.getWidth() > x
							&& j <= y && j + item.getHeight() > y)
						return item;
					else if(item.getRotation() % 2 == 1 && i <= x && 
							i + item.getHeight() > x
							&& j <= y && j + item.getWidth() > y)
						return item;
				}
			}
		}
		return null;
	}
}
